8.3 Kodierung und Skalierung#
ML-Algorithmen können nur Zahlen verarbeiten. In diesem Kapitel werden wir uns zunächst damit beschäftigen, wie auch kategoriale Daten wie beispielsweise die Farbe eines Autos verarbeitet werden können. Da viele ML-Modelle empfindlich darauf reagieren, wenn die numerischen Werte in sehr unterschiedlichen Größenordnungen liegen, beschäftigen wiruns auch mit der Sklaierung von numerischen Daten.
Lernziele#
Lernziele
Sie können geordnete kategoriale (= ordinale) Daten mit Hilfe eines Dictionaries und der
replace()-Methode als Zahlen kodieren.Sie können ungeordnete kategoriale (= nominame) Daten mit Hilfe der
get_dummies()-Methode als Zahlen kodieren. Diese Methode nennt man One-Hot-Kodierung.Sie können numerische Daten skalieren, indem Sie
mit dem MinMaxScaler die Daten normieren oder
mit dem StandardScaler die Daten standardisieren.
Kodierung von kategorialen Daten#
Bei den Beispielen zur linearen Regression haben wir zur Prognose des Verkaufspreises nur numerische Daten genutzt, wie beispielsweise den Kilometerstand. Es gibt jedoch weitere Merkmale, die die Kaufentscheidung beeinflussen, wie der Kraftstofftyp (Diesel oder Benzin) und die Marke des Autos. Diese würden wir ebenfalls gerne in die Prognose des Preises einfließen lassen. Dazu müssen die kategorialen Daten, die in der Regel durch den Datentyp String gekennzeichnet sind, vorab in Integer oder Floats umgewandelt werden. Je nachdem, ob die kategorialen Daten geordnet oder ungeordnet sind, gibt es verschiedene Vorgehensweisen, wie wir uns im Folgenden anhand eines Beispiels erarbeiten.
Wir laden einen Datensatz mit Verkaufsdaten der Plattform
Autoscout24.de. Sie können die csv-Datei hier
herunterladen Download autoscout24_kodierung.csv und in
das Jupyter Notebook importieren. Alternativ können Sie die csv-Datei auch über
die URL importieren, wie es in der folgenden Code-Zelle gemacht wird. Mit der
Methode .info()lassen wir uns anzeigen, welchen Datentyp die Merkmale haben.
import pandas as pd
url = 'https://gramschs.github.io/book_ml4ing/data/autoscout24_kodierung.csv'
daten = pd.read_csv(url)
daten.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 930 entries, 0 to 929
Data columns (total 14 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Marke 930 non-null object
1 Modell 930 non-null object
2 Farbe 930 non-null object
3 Erstzulassung 930 non-null object
4 Jahr 930 non-null int64
5 Preis (Euro) 930 non-null int64
6 Leistung (kW) 930 non-null int64
7 Leistung (PS) 930 non-null int64
8 Getriebe 930 non-null object
9 Kraftstoff 930 non-null object
10 Verbrauch (l/100 km) 930 non-null float64
11 Kilometerstand (km) 930 non-null float64
12 Bemerkungen 930 non-null object
13 Zustand 930 non-null object
dtypes: float64(2), int64(4), object(8)
memory usage: 101.8+ KB
Wir sehen
8 Merkmale mit Datentyp
object: Marke, Modell, Farbe, Erstzulassung, Getriebe, Kraftstoff, Bemerkungen, Zustand,4 Merkmale mit Datentyp
int64: Jahr, Preis (Euro), Leistung (PS), Leistung (kW)2 Merkmale mit Datentyp
float64: Verbrauch (l/100 km) und Kilometerstand (km).
Als erstes betrachten wir geordnete Daten.
Geordnete kategoriale Daten mit zwei Kategorien (binär ordinale Daten)#
Als erstes betrachten wir das Merkmal »Getriebe«. Mit der Methode .unique()
ermitteln wir, wie viele verschiedene Kategorien es für dieses Merkmal gibt.
daten['Getriebe'].unique()
array(['Automatik', 'Schaltgetriebe'], dtype=object)
Es gibt nur zwei Kategorien: Automatik und Schaltgetriebe. Diese beiden Werte wollen wir durch Integer ersetzen:
Automatik –> 0 und
Schaltgetriebe –> 1.
Pandas bietet dazu die Methode replace() an. Bei der Verwendung dieser Methode
darf sich der Datentyp nicht ändern (in Pandas Version 2 noch erlaubt, ab
Version 3 verboten). Daher kodieren wir zunächst die Strings 'Automatik' und
'Schaltgetriebe' als die Strings '0' und '1'mit Hilfe eines Dictionaries:
getriebe_kodierung = {
'Automatik': '0',
'Schaltgetriebe': '1',
}
Dann verwenden wir replace(), um die Ersetzung vorzunehmen. Zuletzt wandeln
wir die Strings '0' und '1' noch mit der Methode astype() in Integer um:
daten['Getriebe'] = daten['Getriebe'].replace(getriebe_kodierung)
daten['Getriebe'] = daten['Getriebe'].astype('int')
# Kontrolle
daten['Getriebe'].unique()
array([0, 1])
Geordnete kategoriale Daten (ordinale Daten)#
Für das Merkmal »Zustand« gibt es vier Kategorien.
daten['Zustand'].unique()
array(['Gebrauchtwagen', 'junger Gebrauchtwagen', 'Neuwagen',
'Jahreswagen'], dtype=object)
Die vier Zustände haben eine Ordnung, denn ein Neuwagen ist wertvoller als ein Jahreswagen. Der Jahreswagen wiederum ist im Allgmeinen wertvoller als der junge Gebrauchtwagen. Am wenigsten wertvoll ist der Gebrauchtwagen. Durch diese Ordnung ist es sinnvoll, beim Kodieren der Zustände durch Integer die Ordnung beizubehalten. Ob wir jetzt die 0 für den Neuwagen vergeben und die 3 für den Gebrauchtwagen oder umgekehrt, ist Geschmackssache.
zustand_kodierung = {
'Gebrauchtwagen': '0',
'junger Gebrauchtwagen': '1',
'Jahreswagen': '2',
'Neuwagen': '3'
}
daten['Zustand'] = daten['Zustand'].replace(zustand_kodierung)
daten['Zustand'] = daten['Zustand'].astype('int')
# Kontrolle
daten['Zustand'].unique()
array([0, 1, 3, 2])
Ungeordnete kategoriale Daten (nominale Daten): One-Hot-Kodierung#
Anders verhät es sich bei den ungeordnetem kategorialen Daten wie beispielsweise den Farben der Autos.
daten['Farbe'].unique()
array(['grau', 'grün', 'schwarz', 'blau', 'weiß', 'silber', 'rot',
'braun', 'orange', 'gelb', 'gold', 'beige', 'bronze', 'violett'],
dtype=object)
14 verschiedene Farben haben die Autos in dem Datensatz. Es wäre jedoch falsch,
nun Integer von 0 bis 13 zu vergeben, denn das würde eine Ordnung der Farben
voraussetzen, die es nicht gibt. Wir verwenden daher das Verfahren der
One-Hot-Kodierung. Anstatt einer Spalte mir den Farben führen wir 14 neue
Spalten mit den Farben ‘grau’, ‘grün’, ‘schwarz’, ‘blau’, usw. ein. Wenn ein
Auto die Farbe ‘grau’ hat, notieren wir in der Spalte ‘grau’ in dieser Zeile
eine 1 und in den übrigen 13 Spalten mit den anderen Farben eine 0. So können
wir die Farben numerisch kodieren, ohne eine Ordnung der Farben einzuführen, die
es nicht gibt. Pandas bietet dafür die Methode get_dummies()an. Schauen wir
uns zunächst an, was diese Methode bewirkt.
pd.get_dummies(daten['Farbe'])
| beige | blau | braun | bronze | gelb | gold | grau | grün | orange | rot | schwarz | silber | violett | weiß | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | False | False | False | False | False | False | True | False | False | False | False | False | False | False |
| 1 | False | False | False | False | False | False | False | True | False | False | False | False | False | False |
| 2 | False | False | False | False | False | False | False | False | False | False | True | False | False | False |
| 3 | False | False | False | False | False | False | False | False | False | False | True | False | False | False |
| 4 | False | False | False | False | False | False | False | False | False | False | True | False | False | False |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 925 | False | False | False | False | False | False | True | False | False | False | False | False | False | False |
| 926 | False | False | False | False | False | False | True | False | False | False | False | False | False | False |
| 927 | False | False | False | False | False | False | False | False | False | False | True | False | False | False |
| 928 | False | False | False | False | False | False | True | False | False | False | False | False | False | False |
| 929 | False | True | False | False | False | False | False | False | False | False | False | False | False | False |
930 rows × 14 columns
Damit haben wir die Spalte »Farbe« nun durch 14 Spalten kodiert. Wir könnten nun
im ursprünglichen Datensatz die Spalte »Farbe« löschen und die neuen 14 Spalten
hinzufügen. Tatsächlich erledigt das Pandas bereits für uns, wenn wir die
Methode etwas modifiziert aufrufen. Mit dem Argument data= übergeben wir nun
den kompletten Datensatz und mit dem Argument columns= spezifizieren wir die
Liste der ungeordneten kategorialen Daten, die One-Hot-kodiert werden sollen.
daten = pd.get_dummies(data=daten, columns=['Farbe'])
daten.head()
| Marke | Modell | Erstzulassung | Jahr | Preis (Euro) | Leistung (kW) | Leistung (PS) | Getriebe | Kraftstoff | Verbrauch (l/100 km) | ... | Farbe_gelb | Farbe_gold | Farbe_grau | Farbe_grün | Farbe_orange | Farbe_rot | Farbe_schwarz | Farbe_silber | Farbe_violett | Farbe_weiß | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ford | Ford S-Max | 12/2018 | 2018 | 19350 | 140 | 190 | 0 | Diesel | 5.2 | ... | False | False | True | False | False | False | False | False | False | False |
| 1 | mercedes-benz | Mercedes-Benz G 500 | 10/2018 | 2018 | 119900 | 310 | 421 | 0 | Benzin | 11.5 | ... | False | False | False | True | False | False | False | False | False | False |
| 2 | mercedes-benz | Mercedes-Benz E 250 | 05/2012 | 2012 | 18997 | 150 | 204 | 1 | Diesel | 5.1 | ... | False | False | False | False | False | False | True | False | False | False |
| 3 | bmw | BMW 325 | 05/2003 | 2003 | 11000 | 141 | 192 | 1 | Benzin | 9.6 | ... | False | False | False | False | False | False | True | False | False | False |
| 4 | ford | Ford S-Max | 04/2017 | 2017 | 21980 | 132 | 179 | 0 | Diesel | 5.0 | ... | False | False | False | False | False | False | True | False | False | False |
5 rows × 27 columns
Die neuen Spaltennamen sind eine Kombination aus dem alten Spaltennamen »Farbe« und den Kategorien.
Skalierung von numerischen Daten#
Nachdem wir uns intensiv mit den kategorialen Daten beschäftigt haben, betrachten wir nun die numerischen Daten. Wir laden den Original-Datensatz und entfernen die kategorialen Daten.
url = 'https://gramschs.github.io/book_ml4ing/data/autoscout24_kodierung.csv'
daten = pd.read_csv(url)
daten = daten.drop(columns=['Marke', 'Modell', 'Farbe', 'Erstzulassung',
'Getriebe', 'Kraftstoff','Bemerkungen', 'Zustand'])
daten.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 930 entries, 0 to 929
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Jahr 930 non-null int64
1 Preis (Euro) 930 non-null int64
2 Leistung (kW) 930 non-null int64
3 Leistung (PS) 930 non-null int64
4 Verbrauch (l/100 km) 930 non-null float64
5 Kilometerstand (km) 930 non-null float64
dtypes: float64(2), int64(4)
memory usage: 43.7 KB
Ein erster Blick auf die Daten zeigt bereits, dass die Eigenschaftswerte in unterschiedlichen Bereichen liegen.
daten.head()
| Jahr | Preis (Euro) | Leistung (kW) | Leistung (PS) | Verbrauch (l/100 km) | Kilometerstand (km) | |
|---|---|---|---|---|---|---|
| 0 | 2018 | 19350 | 140 | 190 | 5.2 | 100000.0 |
| 1 | 2018 | 119900 | 310 | 421 | 11.5 | 92500.0 |
| 2 | 2012 | 18997 | 150 | 204 | 5.1 | 128586.0 |
| 3 | 2003 | 11000 | 141 | 192 | 9.6 | 190000.0 |
| 4 | 2017 | 21980 | 132 | 179 | 5.0 | 85349.0 |
Der Verbrauch gemessen in Litern pro 100 Kilometer liegt zwischen 5 und 10, wohingegen der Kilometerstand die 100000 km übersteigt.Das zeigt auch die Übersicht der statistischen Kennzahlen:
daten.describe()
| Jahr | Preis (Euro) | Leistung (kW) | Leistung (PS) | Verbrauch (l/100 km) | Kilometerstand (km) | |
|---|---|---|---|---|---|---|
| count | 930.000000 | 930.000000 | 930.000000 | 930.000000 | 930.000000 | 930.000000 |
| mean | 2016.289247 | 22958.117204 | 119.869892 | 163.067742 | 6.016667 | 85817.368817 |
| std | 5.325487 | 17389.729506 | 58.155680 | 79.036180 | 1.593688 | 74230.260767 |
| min | 2000.000000 | 150.000000 | 37.000000 | 50.000000 | 3.500000 | 1.000000 |
| 25% | 2013.000000 | 11990.000000 | 81.000000 | 110.000000 | 4.900000 | 28277.250000 |
| 50% | 2018.000000 | 18970.000000 | 110.000000 | 150.000000 | 5.700000 | 72055.000000 |
| 75% | 2020.000000 | 28735.000000 | 140.000000 | 190.000000 | 6.700000 | 124839.750000 |
| max | 2023.000000 | 122980.000000 | 450.000000 | 612.000000 | 14.900000 | 400000.000000 |
Damit ist auch der Boxplot nur noch schwer lesbar:
import plotly.express as px
fig = px.box(daten)
fig.show()
Das hat auch Auswirkungen auf das Training der ML-Modelle. Daher beschäftigen wir uns nun mit der Skalierung von Daten.
Sind die Bereich der Daten von ihren Zahlenwerten sehr verschieden, sollten alle numerischen Werte in dieselbe Größenordnung gebracht werden. Dieser Vorgang heißt Skalieren der Daten. Gebräulich sind dabei zwei verschiedene Methoden:
Normierung und
Standardisierung.
Normierung#
Bei der Normierung wird festgelegt, dass alle Zahlenwerte in einem festen Intervall liegen. Besonders häufig wird das Intervall \([0,1]\) genommen. Die Verbrauch (l/ 100 km), der zwischen 3.5 und 14.9 liegt, würde so transformiert werden, dass das Minimum 3.5 der 0 entspricht und das Maximum 14.9 der 1. Genauso würde mit den anderen Eigenschaften verfahren werden. Wir nutzen zur praktischen Umsetzung Scikit-Learn.
Damit keine Informationen über die Testdaten in das Training des ML-Modells sickern (Data Leakage), wird die Normierung an das Minimum und das Maximum der Trainingsdaten angepasst und ggf. für die Testdaten angewendet. Damit können einzelne Testdaten auch außerhalb des Intervalls \([0,1]\) liegen. Wir splitten daher zunächst unsere Daten in Trainings- und Testdaten.
from sklearn.model_selection import train_test_split
daten_train, daten_test = train_test_split(daten, random_state=0)
Dann importieren wir die Klasse MinMaxScaler aus dem Untermodul
sklearn.preprocessing und erzeugen ein MinMaxScaler-Objekt:
from sklearn.preprocessing import MinMaxScaler
# Auswahl Skalierungsmethode: Normierung
normierung = MinMaxScaler()
Jetzt wird das Minimum/Maximum jeder Spalte bestimmt, also der MinMaxScaler an
die Trainingsdaten angepasst. Daher ist es nicht verwunderlich, dass die Methode
fit() genannt wurde. Dem MinMaxScaler werden also die Trainingsdaten
übergeben:
normierung.fit(daten_train)
MinMaxScaler()In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
MinMaxScaler()
Zuletzt erfolgt die Transformation der Daten mit der transform()-Methode. Dazu
werden einmal die Trainingsdaten und einmal die Testdaten dem angepassten
MinMaxScaler übergeben und die transformierten Daten in neuen Variablen
gespeichert.
# Transformation der Trainungs- und Testdaten
X_train_normiert = normierung.transform(daten_train)
X_test_normiert = normierung.transform(daten_test)
Wir schauen in ‘X_train_normiert’ hinein:
print(X_train_normiert)
[[0.73913043 0.19881137 0.24939467 0.24911032 0.10619469 0.2603238 ]
[0.52173913 0.46282667 0.60048426 0.59964413 1. 0.43312217]
[0.39130435 0.054954 0.19612591 0.19572954 0.26548673 0.54499772]
...
[0.52173913 0.11642107 0.10411622 0.10498221 0.07079646 0.29836399]
[0.52173913 0.06622975 0.20096852 0.20106762 0.15044248 0.48629743]
[0.56521739 0.140438 0.29782082 0.29893238 0.20353982 0.36249681]]
Die Normierung der Daten scheint funktioniert zu haben. Alle Werte liegen zwischen 0 und 1. Gleichzeitig haben wir aber die Pandas-DataFrame-Datenstruktur verloren. Die Normierung ist nicht für uns Menschen gedacht, sondern für den ML-Algorithmus. Daher nutzt Scikit-Learn die Transformation der Daten gleichzeitig für die Umwandlung in das speichereffizientere NumPy-Array, das für den ML-Algorithmus gebraucht wird.
Standardisierung#
Oft sind Daten normalverteilt. Die Standardisierung berücksichtigt das und transformiert nicht auf ein festes Intervall, sondern verschiebt den Mittelwert auf 0 und die Varianz auf 1. Die normalverteilten Daten werden also standardnormalverteilt. Auch das lassen wir Scikit-Learn erledigen:
from sklearn.preprocessing import StandardScaler
# Auswahl Skalierungsmethode: Standardisierung
standardisierung = StandardScaler()
# Analyse: jede Spalte wird auf ihr Minimum und ihre Maximum hin untersucht
# es werden immer die Trainingsdaten verwendet
standardisierung.fit(daten_train)
# Transformation der Trainungs- und Testdaten
X_train_standardisiert = standardisierung.transform(daten_train)
X_test_standardisiert = standardisierung.transform(daten_test)
print(X_train_standardisiert)
[[ 1.05411060e-01 6.39004580e-02 3.46860470e-01 3.41219572e-01
-7.59326677e-01 2.43172311e-01]
[-8.53747017e-01 1.90756199e+00 2.88123391e+00 2.87447279e+00
5.74521458e+00 1.17279526e+00]
[-1.42924186e+00 -9.40678755e-01 -3.76651551e-02 -4.45550293e-02
3.99898498e-01 1.77466489e+00]
...
[-8.53747017e-01 -5.11444287e-01 -7.01845780e-01 -7.00371851e-01
-1.01693227e+00 4.47821435e-01]
[-8.53747017e-01 -8.61938393e-01 -2.70828013e-03 -5.97756914e-03
-4.37319684e-01 1.45886827e+00]
[-6.61915402e-01 -3.43730157e-01 6.96429220e-01 7.01275866e-01
-5.09112922e-02 7.92844041e-01]]
Auch hier geht die Pandas-DataFrame-Struktur verloren.
Zusammenfassung und Ausblick#
Kategoriale Daten müssen kodiert werden, damit sind in einem ML-Algorithmus
verarbeitet werden können. Geordnete kategoriale (ordinale) Daen können dabei
über ein Dictionary und die replace()-Methode kodiert werden. Für ungeordnete
kategoriale (nominale) Daten muss die One-Hot-Kodierung verwendet werden.
Auch numerische Daten müssen häufig für ML-Algorithmen aufbereitet werden, vor allem, wenn die Daten in sehr unterschiedlichen Zahlenbereichen liegen. Bei den bisher eingeführten ML-Modellen lineare Regression und Entscheidungsbäumen ist die Skalierung der numerischen Daten nicht notwendig. Erst die nachfolgenden ML-Modelle werden davon Gebrauch machen.